#!/usr/bin/env python

import wx
import wx.grid
import wx.html
import wx.aui as aui
import string,re
import requests,json,sys
from requests_toolbelt import MultipartEncoder
from six import BytesIO
from threading import Thread
import time, queue
from urllib.parse import unquote, quote
from serialCom import SerialCom

SW_VERSION = '1.0.0'

ID_Settings = wx.NewIdRef()
ID_BAUD_SETTING = wx.NewIdRef()
ID_BAUD_RATE_9600 = wx.NewIdRef()
ID_BAUD_RATE_38400 = wx.NewIdRef()
ID_BAUD_RATE_57600 = wx.NewIdRef()
ID_BAUD_RATE_115200 = wx.NewIdRef()
ID_About = wx.NewIdRef()

ALPHA_ONLY = 1
DIGIT_ONLY = 2
DIGIT_DOT  = 3
MAC_ADDR   = 4
DIGIT_SPACE = 5
mac_string = ('0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','A','B','C','D','E','F','-')
rotationLine = ('|', '/', '-', '\\')


#----------------------------------------------------------------------
# My app icon
def GetMondrianData():
    return \
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00 \x00\x00\x00 \x08\x06\x00\
\x00\x00szz\xf4\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\x00qID\
ATX\x85\xed\xd6;\n\x800\x10E\xd1{\xc5\x8d\xb9r\x97\x16\x0b\xad$\x8a\x82:\x16\
o\xda\x84pB2\x1f\x81Fa\x8c\x9c\x08\x04Z{\xcf\xa72\xbcv\xfa\xc5\x08 \x80r\x80\
\xfc\xa2\x0e\x1c\xe4\xba\xfaX\x1d\xd0\xde]S\x07\x02\xd8>\xe1wa-`\x9fQ\xe9\
\x86\x01\x04\x10\x00\\(Dk\x1b-\x04\xdc\x1d\x07\x14\x98;\x0bS\x7f\x7f\xf9\x13\
\x04\x10@\xf9X\xbe\x00\xc9 \x14K\xc1<={\x00\x00\x00\x00IEND\xaeB`\x82'

def GetMondrianBitmap():
    return wx.Bitmap(GetMondrianImage())

def GetMondrianImage():
    stream = BytesIO(GetMondrianData())
    return wx.Image(stream)

def GetMondrianIcon():
    icon = wx.Icon()
    icon.CopyFromBitmap(GetMondrianBitmap())
    return icon

#---------------------------------------------------------------------------
# Show how to derive a custom wxLog class
class MyLog(wx.Log):
    def __init__(self, textCtrl, logTime=0):
        wx.Log.__init__(self)
        self.tc = textCtrl
        self.logTime = logTime

    def DoLogText(self, message):
        if self.tc:
            self.tc.AppendText(message + '\n')


#########################################################################################
# My Main Entry Class
#########################################################################################
class MyMainFrame(wx.Frame):
    """
    This is the main frame Entry of the application.
    """
    def __init__(self, parent, id=-1, title="", pos=wx.DefaultPosition,
                 size = (970, 720), style=wx.DEFAULT_FRAME_STYLE |
                                            wx.SUNKEN_BORDER |
                                            wx.CLIP_CHILDREN):

        wx.Frame.__init__(self, parent, id, title, pos, size, style)

        # tell FrameManager to manage this frame
        self._mgr = aui.AuiManager()
        self._mgr.SetManagedWindow(self)
        self.SetIcon(GetMondrianIcon())

        ########################################################################################
        # create menu
        ########################################################################################
        mb = wx.MenuBar()

        file_menu = wx.Menu()
        file_menu.Append(wx.ID_EXIT, "Exit")

        baud_setting_menu = wx.Menu()
        baud_setting_menu.AppendRadioItem(ID_BAUD_RATE_9600, '9600')
        baud_setting_menu.AppendRadioItem(ID_BAUD_RATE_38400, '38400')
        baud_setting_menu.AppendRadioItem(ID_BAUD_RATE_57600, '57600')
        baud_setting_menu.AppendRadioItem(ID_BAUD_RATE_115200, '115200')
        # baud_setting_menu.AppendSeparator()
        baud_setting_menu.Check(ID_BAUD_RATE_115200, True)
        self.baudRate = 115200

        setting_menu = wx.Menu()
        # setting_menu.Append(wx.ID_ANY, 'Baud Rate', baud_setting_menu)
        setting_menu.AppendSubMenu(baud_setting_menu, 'Baud Rate')

        help_menu = wx.Menu()
        help_menu.Append(ID_About, "About...")

        mb.Append(file_menu, "File")
        mb.Append(setting_menu, "Setting")
        mb.Append(help_menu, "Help")
        self.SetMenuBar(mb)
        ########################################################################################

        self.sb = CustomStatusBar(self)
        self.SetStatusBar(self.sb)
        # self.sb.showSerialStatus('COM1', str(self.baudRate))

        # min size for the frame itself isn't completely done.
        # see the end up FrameManager::Update() for the test
        # code. For now, just hard code a frame minimum size
        self.SetMinSize(wx.Size(400, 400))

        # But instead of the above we want to show how to use our own wx.Log class
        self.log = self.CreateLogTextCtrl()
        wx.Log.SetActiveTarget(MyLog(self.log))

        self.tx_queue = queue.Queue(100)
        self.rx_queue = queue.Queue(100)
        self.rxDataCheckThreadRunning = False
        self.rxDataCheckThread = None
        self.serial = SerialCom(self.rx_queue, self.tx_queue, wx.LogMessage)
        self.serialCtrlPannel = SerialControllerPannel(self, self, self.serial)
        self.serialCtrlPannel.setBaudRate(self.baudRate)
        self.infoPannel = InfoPannel(self, self)

        self._mgr.AddPane(self.serialCtrlPannel,
                          aui.AuiPaneInfo().
                          Name("serialControlPannle").Caption("Setting").
                          Left().Layer(1).Position(1).
                          BestSize((200, -1)).MinSize((200, -1)).
                          CloseButton(False))
        self._mgr.AddPane(self.infoPannel,
                          aui.AuiPaneInfo().
                          Name("outputPannel").Caption("Infos").
                          CenterPane().
                          CloseButton(False).
                          Floatable(False))
                          # PaneBorder())

        # self._mgr.AddPane(self.CreateLogTextCtrl(), aui.AuiPaneInfo().
        #                   Name("mylogarea").Caption("Log Pane").
        #                   Bottom().Layer(1).Position(1).CloseButton(False).MaximizeButton(False))

        self._mgr.AddPane(self.log,
                          aui.AuiPaneInfo().
                          Name("logWindow").Caption("Log Pane").
                          Bottom().Layer(1).Position(1).
                          BestSize((-1, 140)).MinSize((-1, 140)).
                          Floatable(False).
                          CloseButton(False))

        # "commit" all changes made to FrameManager
        self._mgr.Update()

        # self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_CLOSE, self.OnClose)

        # Show How To Use The Closing Panes Event
        self.Bind(aui.EVT_AUI_PANE_CLOSE, self.OnPaneClose)

        self.Bind(wx.EVT_MENU, self.OnExit, id=wx.ID_EXIT)
        self.Bind(wx.EVT_MENU, self.OnAbout, id=ID_About)
        self.Bind(wx.EVT_MENU, lambda x:self.OnBaudRate(9600), id=ID_BAUD_RATE_9600)
        self.Bind(wx.EVT_MENU, lambda x:self.OnBaudRate(38400), id=ID_BAUD_RATE_38400)
        self.Bind(wx.EVT_MENU, lambda x:self.OnBaudRate(57600), id=ID_BAUD_RATE_57600)
        self.Bind(wx.EVT_MENU, lambda x:self.OnBaudRate(115200), id=ID_BAUD_RATE_115200)

        self.rxDataCheckThreadStart()

        ##################################################################################################

        # self.timer = wx.Timer(self)
        # self.timer.Start(500)
        # self.Bind(wx.EVT_TIMER, self.OnTimerOut)
        # self.timer.Stop()

    def OnTimerOut(self, event):
        pass

    def rxDataCheckThreadStart(self):
        def rxDataCheck():
            while self.rxDataCheckThreadRunning:
                if (data := self.rx_queue.get(block=True)):
                    self.infoPannel.updataRxData(data)
        self.rxDataCheckThread = Thread(target=rxDataCheck)
        self.rxDataCheckThread.daemon = True
        self.rxDataCheckThreadRunning = True
        self.rxDataCheckThread.start()

    def rxDataCheckThreadStop(self):
        self.rxDataCheckThreadRunning = False
        self.rx_queue.put(None)

    def OnSize(self, event):
        event.Skip()

    def OnPaneClose(self, event):
        caption = event.GetPane().caption
        msg = "Are You Sure You Want To Close This Pane?"
        dlg = wx.MessageDialog(self, msg, "AUI Question",
                                wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION)
        if dlg.ShowModal() in [wx.ID_NO, wx.ID_CANCEL]:
            event.Veto()
        dlg.Destroy()

    def OnClose(self, event):
        self.rxDataCheckThreadStop()
        self.serial.closeCom()
        self.sb.stop()
        self._mgr.UnInit()
        del self._mgr
        self.Destroy()

    def OnBaudRate(self, baudRate=115200):
        self.baudRate = baudRate
        self.serialCtrlPannel.setBaudRate(self.baudRate)
        print(f"Baud Rate : {self.baudRate}")


    def OnExit(self, event):
        print("Exit Requested")
        self.Close()

    def OnAbout(self, event):
        msg = "串口调试工具。\n" + \
              "(c) Copyright 2022-2024, ...."
        dlg = wx.MessageDialog(self, msg, "关于",
                               wx.OK | wx.ICON_INFORMATION)
        dlg.ShowModal()
        dlg.Destroy()

    def DoUpdate(self):
        self._mgr.Update()

    def CreateLogTextCtrl(self):
        tc = wx.TextCtrl(self, -1, style = wx.TE_MULTILINE|wx.TE_READONLY|wx.HSCROLL)
        wx.Platform == "__WXMAC__" and tc.MacCheckSpelling(False)
        return tc


class CustomStatusBar(wx.StatusBar):
    def __init__(self, parent):
        wx.StatusBar.__init__(self, parent, -1)

        # This status bar has three fields
        self.SetFieldsCount(3)
        self.SetStatusWidths([-2, 100, 140])
        self.sizeChanged = False
        self.Bind(wx.EVT_SIZE, self.OnSize)

        # Field 0 ... just text
        self.SetStatusText("欢迎使用串口通信工具!", 0)

        # We're going to use a timer to drive a 'clock' in the last field.
        self.running = True
        self.timer = wx.PyTimer(self.Notify)
        self.timer.Start(500)
        self.Notify()

    def SetStatusLedColour(self, colour):
        self.label.SetBackgroundColour(colour)

    def stop(self):
        self.timer.Stop()
        self.running = False

    # Handles events from the timer we started in __init__().
    # We're using it to drive a 'clock' in field 2 (the third field).
    def Notify(self):
        global token
        if not self.running: return

        t = time.localtime(time.time())
        st = time.strftime("%d-%b-%Y  %I:%M:%S", t)
        self.SetStatusText(st, 2)

    def showSerialStatus(self, port:str = '', rate:str = ''):
        if port:
            self.SetStatusText(f'{port} : {rate}', 1)
        else:
            self.SetStatusText('', 1)

    def OnSize(self, evt):
        evt.Skip()

class SerialControllerPannel(wx.Panel):
    def __init__(self, parent, frame, serial):
        wx.Panel.__init__(self, parent, wx.ID_ANY, wx.DefaultPosition,
                          wx.DefaultSize)
        self.mainFrame = frame
        self.sc = serial
        self.baudrate = 115200
        self.comportConnected = False

        titleFont = wx.Font(18, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
        vert = wx.BoxSizer(wx.VERTICAL)
        vert.AddSpacer(10)

        box = wx.BoxSizer(wx.HORIZONTAL)
        text1 = wx.StaticText(self, wx.ID_ANY, u"串 口:", wx.DefaultPosition, wx.Size(40, 24), style=wx.ALIGN_CENTER)
        self.comCB = wx.ComboBox(self, wx.ID_ANY, "", wx.DefaultPosition, (90, -1), [], wx.CB_DROPDOWN)
        self.comport = None
        self.comCB.Bind(wx.EVT_COMBOBOX, self.EvtComboBox)
        self.comCB.Bind(wx.EVT_COMBOBOX_DROPDOWN, self.EvtComboBoxDD)
        self.comButton = wx.Button(self, wx.ID_ANY, u"打开串口", wx.DefaultPosition, wx.DefaultSize)
        self.comButton.Bind(wx.EVT_BUTTON, self.OnButtonComOpenClick)
        # box.AddSpacer(2)
        box.Add(text1, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL, border=5)
        box.Add(self.comCB, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL, border=5)
        box.AddSpacer(4)
        box.Add(self.comButton, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL, border=5)
        vert.Add(box, proportion=0, flag=wx.ALL, border=5)
        vert.AddSpacer(10)

        box = wx.StaticBox(self, -1, u"接收控制：")
        sbox = wx.StaticBoxSizer(box, wx.VERTICAL)
        self.rxHexCheck = wx.CheckBox(self, wx.ID_ANY, u'HEX显示')
        self.rxHexCheck.Bind(wx.EVT_CHECKBOX, self.OnRxEvtCheckBox)
        sbox.AddSpacer(5)
        sbox.Add(self.rxHexCheck, proportion=0, flag=wx.EXPAND, border=5)
        sbox.AddSpacer(5)
        vert.Add(sbox, proportion=0, flag=wx.EXPAND|wx.ALL, border=5)
        vert.AddSpacer(10)

        box = wx.StaticBox(self, -1, u"发送控制：")
        sbox = wx.StaticBoxSizer(box, wx.VERTICAL)
        self.txHexCheck = wx.CheckBox(self, wx.ID_ANY, u'HEX数据(以空格分隔)')
        self.txHexCheck.Bind(wx.EVT_CHECKBOX, self.OnTxEvtCheckBox)
        self.txEndCheck = wx.CheckBox(self, wx.ID_ANY, u'End with CR')
        self.txEndCheck.Bind(wx.EVT_CHECKBOX, self.OnTxEndEvtCheckBox)
        sbox.AddSpacer(5)
        sbox.Add(self.txHexCheck, proportion=0, flag=wx.EXPAND, border=5)
        sbox.AddSpacer(5)
        sbox.Add(self.txEndCheck, proportion=0, flag=wx.EXPAND, border=5)
        sbox.AddSpacer(5)
        vert.Add(sbox, proportion=0, flag=wx.EXPAND|wx.ALL, border=5)
        vert.AddSpacer(10)

        self.SetSizer(vert)
        vert.Fit(self)
        self.GetSizer().SetSizeHints(self)


    @property
    def serialComReady(self):
        return self.comportConnected

    def setBaudRate(self, rate):
        self.baudrate = rate

    def OnRxEvtCheckBox(self, evt):
        # print(evt.IsChecked())
        self.mainFrame.infoPannel.setRxDataFormat(evt.IsChecked())

    def OnTxEvtCheckBox(self, evt):
        self.mainFrame.infoPannel.setTxDataFormat(evt.IsChecked())

    def OnTxEndEvtCheckBox(self, evt):
        self.mainFrame.infoPannel.setTxEndWithCR(evt.IsChecked())

    # serial comm combo select callback
    def EvtComboBox(self, event):
        cb = event.GetEventObject()
        self.comport = cb.GetString(event.GetSelection())
        wx.LogMessage("Select : %s" % self.comport)

    # serial comm combo dropdown callback
    def EvtComboBoxDD(self, event):
        cb = event.GetEventObject()
        # remove all items
        cb.Clear()
        # add item
        coms = self.sc.getComList()
        for com in coms: cb.Append(com)

    def OnButtonComOpenClick(self, event):
        button = event.GetEventObject()
        text = button.GetLabel()
        if text == u"打开串口":
            if self.comport:
                if self.sc.openCom(self.comport, self.baudrate):
                    button.SetLabel(u"关闭串口")
                    self.comCB.Disable()
                    self.comportConnected = True
                    wx.LogMessage("Open serial port (%s) successfully" % self.comport)
                    # start receiving thread/writing thread/heartbeat thread
                    self.sc.openReadThread()
                    self.sc.openWriteThread()
                    wx.CallAfter(self.mainFrame.sb.showSerialStatus, self.comport, self.baudrate)
                    # self.mainFrame.sb.showSerialStatus(self.comport, self.baudrate)
                else:
                    self.comportConnected = False
                    wx.LogMessage("Open serial port (%s) failed" % self.comport)
            else:
                wx.LogMessage(u"请选择要打开的串口")
        else:
            # self.mainFrame.sb.showSerialStatus()
            wx.CallAfter(self.mainFrame.sb.showSerialStatus, '', self.baudrate)
            self.sc.closeCom()
            button.SetLabel(u"打开串口")
            wx.LogMessage(f"Serial port ({self.comport}) closed.")
            self.comCB.Enable()
            self.comCB.SetValue("")
            self.comport = None
            self.comportConnected = False


class InfoPannel(wx.Panel):

    def __init__(self, parent, frame):

        wx.Panel.__init__(self, parent, wx.ID_ANY, wx.DefaultPosition,
                          wx.DefaultSize)
        self.mainFrame = frame
        self.rxHexFormat = False
        self.txHexFormat = False
        self.endWithEnter = False

        vert = wx.BoxSizer(wx.VERTICAL)
        vert.AddSpacer(10)

        # box = wx.BoxSizer(wx.VERTICAL)
        box = wx.StaticBox(self, -1, u"接收信息：")
        sbox = wx.StaticBoxSizer(box, wx.VERTICAL)
        self.tc_rx = wx.TextCtrl(self, -1, "", style=wx.TE_MULTILINE|wx.TE_READONLY)
        wx.CallAfter(self.tc_rx.SetInsertionPoint, 0)
        btn_clear = wx.Button(self, wx.ID_ANY, u"清空", wx.DefaultPosition, wx.Size(-1, -1))
        btn_clear.Bind(wx.EVT_BUTTON, self.OnButtonRxClearClicked)
        sbox.Add(self.tc_rx, 1, wx.EXPAND)
        sbox.Add(btn_clear, proportion=0, flag=wx.ALL, border=5)
        vert.Add(sbox, proportion=1, flag=wx.EXPAND|wx.ALL, border=5)
        vert.AddSpacer(8)

        # box = wx.BoxSizer(wx.VERTICAL)
        box = wx.StaticBox(self, -1, u"发送信息：")
        sbox = wx.StaticBoxSizer(box, wx.VERTICAL)
        self.tc_tx = wx.TextCtrl(self, -1, "", style=wx.TE_MULTILINE)
        wx.CallAfter(self.tc_tx.SetInsertionPoint, 0)
        btn_clear = wx.Button(self, wx.ID_ANY, u"清空", wx.DefaultPosition, wx.Size(-1, -1))
        btn_clear.Bind(wx.EVT_BUTTON, self.OnButtonTxClearClicked)
        btn_send = wx.Button(self, wx.ID_ANY, u"发送", wx.DefaultPosition, wx.Size(-1, -1))
        btn_send.Bind(wx.EVT_BUTTON, self.OnButtonTxSendClicked)
        hbox = wx.BoxSizer(wx.HORIZONTAL)
        hbox.Add(btn_clear, 0, wx.ALL|wx.EXPAND, 5)
        hbox.AddSpacer(20)
        hbox.Add(btn_send, 0, wx.ALL|wx.EXPAND, 5)
        sbox.Add(self.tc_tx, 1, wx.EXPAND)
        sbox.Add(hbox, proportion=0, flag=wx.ALL, border=5)
        vert.Add(sbox, proportion=1, flag=wx.EXPAND|wx.ALL, border=5)
        vert.AddSpacer(10)

        self.SetSizer(vert)
        vert.Fit(self)
        self.GetSizer().SetSizeHints(self)

    def setRxDataFormat(self, hexFormat:bool=False)->None:
        self.rxHexFormat = hexFormat

    def setTxDataFormat(self, hexFormat:bool=False)->None:
        self.txHexFormat = hexFormat

    def setTxEndWithCR(self, endWithCR:bool=False)->None:
        self.endWithEnter = endWithCR

    # append rx data to tx msg output area
    def updataRxData(self, data:bytes):
        # print(data)
        if self.rxHexFormat:
            wx.CallAfter(lambda:self.tc_rx.AppendText(' '.join([f'{d:02x}' for d in data])))

        else:
            try:
                wx.CallAfter(lambda:self.tc_rx.AppendText(data.decode('utf-8')))
            except Exception as e:
                pass

    def OnButtonRxClearClicked(self, event):
        self.tc_rx.Clear()

    def OnButtonTxClearClicked(self, event):
        self.tc_tx.Clear()

    def OnButtonTxSendClicked(self, event):
        # print(self.tc_tx.GetValue())
        if self.txHexFormat:
            s = self.tc_tx.GetValue().strip().split(' ')
            # print(s)
            try:
                o = list(map(lambda x:int(x, 16), s))
                self.endWithEnter and o.extend([ord('\r'), ord('\n')])
                o = bytes(o)
            except Exception as e:
                wx.LogMessage('Invalid HEX format, HEX str must be seperated by \' \', eg. : 12 aa bc')
            else:
                self.mainFrame.serial.sendData(o)
        else:
            s = self.tc_tx.GetValue() + ('\r\n' if self.endWithEnter else '')
            # call mainFrame serial method to send out data
            try:
                self.mainFrame.serial.sendData(bytes(s, encoding='utf-8'))
            except Exception as e:
                pass


#----------------------------------------------------------------------
if __name__ == '__main__':
    import multiprocessing as mp
    import sys,os,ctypes
    # Add support for when a program which uses multiprocessing has been frozen to produce a Windows executable.
    mp.freeze_support()

    class MyApp(wx.App):
        def OnInit(self):
            # setting current window'sIcon as this process taskbar icon
            if os.name == 'nt':
                ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("myappid-server")
            frame = MyMainFrame(None, -1, u'串口通信程序-'+str(SW_VERSION), size=wx.Size(800, 600))
            # set main frame icon
            icon = wx.Icon(name='.\\icon\\wxpdemo.ico', type=wx.BITMAP_TYPE_ICO)
            frame.SetIcon(icon)
            frame.Show(True)
            self.SetTopWindow(frame)
            return True

    app = MyApp(False)
    app.MainLoop()

